#/usr/bin/perl -w
package WaferMap;

use strict;
use warnings;
use Win32::OLE;
use Win32::OLE qw( with in );
use Win32::OLE::Const 'Microsoft Excel';

## user modules
use SimpleError;
use CPReport;

## constants
use constant DEFAULT_NA_COLOR => 1;
use constant DEFAULT_OTHER_COLOR => 15;
use constant +{
    ADD_COORD => 1,
};

our $Excel;

sub new {
    my $class = shift;
    my $self = +{ @_ };
    bless $self, $class;
    return $self;
}

sub initialize {
    my $self = shift;
    ## Excel object
    unless (defined($Excel)) {
        $Excel = Win32::OLE->GetActiveObject('Excel.Application')
            || Win32::OLE->new( 'Excel.Application', 'Quit' );
        if (defined $Excel) {
            message("got the Excel application OLE object: $Excel");
        } else {
            error("cannot get the Excel application object: " . Win32::OLE->LastError() );
        }
    }
    ## Excel configuration
    $Win32::OLE::Warn = 3; # die on errors
    $self->{Original}{SheetsInNewWorkbook} = $Excel->{SheetsInNewWorkbook};
    $Excel->{SheetsInNewWorkbook} = 0 + @{ $self->{Maps} };
    message( sprintf("adding workbook with %d worksheets", scalar(@{ $self->{Maps} })) );
    $self->{Workbook} = $Excel->Workbooks->Add();
    error("cannot create new workbook: " . Win32::OLE->LastError()) unless defined($self->{Workbook});
    $self->{Initialized} = 1;
    return $self->{Workbook};
}

sub finalize {
    my $self = shift;
    $Excel->{SheetsInNewWorkbook} = 0 + $self->{Original}{SheetsInNewWorkbook};
    $self->{Workbook}->Worksheets(1)->Activate();
    1;
}

sub save {
    my $self = shift;
    error("no file name specified to save") unless defined $self->{SaveAs};
    message("saving workbook to file: $self->{SaveAs}");
    $Excel->{DisplayAlerts} = 0; # no 'File already exists' when saving, or others alike
    $self->{Workbook}->SaveAs( $self->{SaveAs} );
    $Excel->{DisplayAlerts} = 1;
    $self->{Workbook}->Close();
    1;
}

sub set {
    my $self = shift;
    %{ $self } = ( %$self, @_ );
    return $self;
}

sub add_map {
    my $self = shift;
    SimpleError::assert(ref($_[0]) eq 'HASH');
    my %map_setup = %{ $_[0] };
    check_map_setup(\%map_setup, 1);
    push @{ $self->{Maps} }, \%map_setup;
    return \%map_setup;
}

### class method
sub check_map_setup {
    my $self = shift;
    my ($setup, $mode) = @_;
    $mode = 0 unless defined($mode);
    my @stage1 = qw( Compare Coordinate Sheet_Name Map_Data/ARRAY
        Map_Origin/ARRAY Ranges/ARRAY Colors/ARRAY Field_Origin/ARRAY Field_Direct );
    my @stage2 = qw( Matrix/HASH Boundary/ARRAY Comp_Sub/CODE );
    my @todo;
    push @todo, @stage1 if ($mode & 1);
    push @todo, @stage2 if ($mode & 2);
    for ( @todo) {
        if (/(\w+)\/([\w:]+)/) {
            my ($setting, $type) = ($1, $2);
            error("$setting: ($type): not correctly set") unless ref($setup->{$setting}) eq $type;
        } else {
            error("$_: not defined") unless defined($setup->{$_});
        }
    }
    return 1;
}

sub execute {
    my $self = shift;
    my $num_maps = @{ $self->{Maps} };
    $self->initialize() unless $self->{Initialized};
    for my $i (1 .. $num_maps) {
        my $map_setup = $self->{Maps}[$i - 1];
        ## configure map settings (collecte data, etc.)
        $self->{Workmap} = $map_setup;
        $self->prepare_map();
        check_map_setup($self->{Workmap}, 2); # stage 2 check
        ## select worksheet
        $self->{Worksheet} = $self->{Workbook}->Worksheets($i + 0);
        $self->{Worksheet}->Activate();
        message("getting worksheet: $self->{Worksheet}");
        $self->{Worksheet}{Name} = $self->{Workmap}{Sheet_Name};
        $self->fill_map_info();
        $self->draw_map();
    }
    1;
}

sub prepare_map {
    my $self = shift;
    my $map_data = $self->{Workmap}{Map_Data};
    my $table = $self->{DA}->get_collection_ext(@$map_data);
    my %matrix;
    my %y_tbl;
    for my $i (0 .. $#$table) {
        my $row = $table->[$i];
        $matrix{ $row->[0] }{ $row->[1] } = $row->[2];
        $y_tbl{ $row->[1] } = 1;
    }
    my @x_addr = sort {; $a <=> $b; } keys(%matrix);
    my @y_addr = sort {; $a <=> $b; } keys(%y_tbl);
    my @boundary = ($x_addr[0], $y_addr[0], $x_addr[$#x_addr], $y_addr[$#y_addr]);
    SimpleError::assert(scalar(@$table) <= $self->{Gross_Dies}) if defined($map_data->[2]);
    SimpleError::assert(scalar(@$table) == $self->{Gross_Dies}) if ! defined($map_data->[2]);
    message("$self->{Workmap}{Sheet_Name}: fill dies: " . scalar(@$table) . "; option: " . $self->{Workmap}{Map_Option});
    message( sprintf("matrix boundary: (%3d,%3d) --- (%3d,%3d)", @boundary) );
    $self->{Workmap}{Matrix} = \%matrix;
    $self->{Workmap}{Boundary} = \@boundary;
    $self->{Workmap}{Draw_Dies} = scalar(@$table);
    $self->{Workmap}{Comp_Sub} = CPReport::get_range_sort_sub($self->{Workmap}{Compare});
    if (defined($self->{Workmap}{Ranges})) {
        $self->{Workmap}{Range_Desc} = CPReport::get_range_desc($self->{Workmap}{Compare}, $self->{Workmap}{Ranges});
        $self->{Workmap}{Range_Colors} = $self->get_range_colors();
        $self->{Workmap}{Range_Count} = [
            map 0, (scalar(@{$self->{Workmap}{Ranges}}) + (($self->{Workmap}{Compare} & 4) ? 1 : 0)) ];
    }
    return 1;
}

sub fill_map_info {
    my $self = shift;
    my ($row, $col) = (1, 1);
    $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Value} = 'Wafer ID';
    $self->{Worksheet}->Cells(0 + $row, 1 + $col)->{Value} = $self->{DA}{Data}{Lot_Number};
    $row++;
    $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Value} = 'Gross Dies';
    $self->{Worksheet}->Cells(0 + $row, 1 + $col)->{Value} = $self->{DA}{Data}{Gross_Dies};
    $row++;
    $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Value} = 'Draw Dies';
    $self->{Worksheet}->Cells(0 + $row, 1 + $col)->{Value} = $self->{Workmap}{Draw_Dies};
    $row += 2;
    if (defined($self->{Workmap}{Ranges})) {
        $self->{Workmap}{Limit_Row} = $row;
        $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Value} = 'Limit';
        $self->{Worksheet}->Cells(0 + $row, 1 + $col)->{Value} = 'Color';
        $row++;
        for my $i (0 .. $#{ $self->{Workmap}{Range_Desc} }) {
            $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Value} = $self->{Workmap}{Range_Desc}[$i];
            #$self->{Worksheet}->Cells(0 + $row, 1 + $col)->{Value} = $self->{Workmap}{Range_Colors}[$i];
            $self->{Worksheet}->Cells(0 + $row, 1 + $col)->{Interior}{ColorIndex} = $self->{Workmap}{Range_Colors}[$i];
            $row++;
        }
        $row += 2;
    }
    $self->{Workmap}{Map_Origin}[0] = $row unless $self->{Workmap}{Map_Origin}[0] >= $row;
    return 1;
}

sub draw_map {
    my $self = shift;
    my $xy = $self->{Workmap}{Coordinate};
    my ($row_init, $col_init) = @{ $self->{Workmap}{Map_Origin} };
    my ($row, $col) = ($row_init, $col_init);
    my ($map_x0, $map_y0, $map_x1, $map_y1) = @{ $self->{Workmap}{Boundary} };
    ## MX, Xadr-coordinate direction
    my @traverse_x = ($xy & 1) ? reverse($map_x0 .. $map_x1) : ($map_x0 .. $map_x1);
    ## MY, Yadr-coordinate direction
    my @traverse_y = ($xy & 2) ? reverse($map_y0 .. $map_y1) : ($map_y0 .. $map_y1);
    ## XY: X first or Y first
    my ($traverse_outer, $traverse_inner);
    my ($x, $y, $p_outer, $p_inner);
    if ($xy & 4) { # Y first
        $p_outer = \$x;
        $traverse_outer = \@traverse_x;
        $p_inner = \$y;
        $traverse_inner = \@traverse_y;
    } else {       # X first
        $p_outer = \$y;
        $traverse_outer = \@traverse_y;
        $p_inner = \$x;
        $traverse_inner = \@traverse_x;
    }
    message("filling cells with worksheet origin: ($row_init, $col_init)");
    for my $outer (@$traverse_outer) {
        ${$p_outer} = $outer;
        for my $inner (@$traverse_inner) {
            ${$p_inner} = $inner;
            $self->draw_cell($x, $y, $row, $col)
                if defined($self->{Workmap}{Matrix}{$x}{$y});
            $col++;
        }
        $row++;
        $col = $col_init;
    }
    print STDERR "\n";
    ## fill count info
    $self->fill_count_info() if defined($self->{Workmap}{Ranges});
    ## adjust cell dimension
    my $map_range = R1C1_to_A1($row_init, $col_init)
        . ':' . R1C1_to_A1($row_init + @$traverse_outer - 1, $col_init + @$traverse_inner - 1);
    message("adjusting format for range: $map_range");
    $self->{Worksheet}->Range($map_range)->{ColumnWidth} = 0 + $self->{Workmap}{Column_Width};
    $self->{Worksheet}->Range($map_range)->{RowHeight} = 0 + $self->{Workmap}{Row_Height};
    $self->{Worksheet}->Range($map_range)->{Orientation} = -90 if $self->{Workmap}{Map_Option} & 0x02;
    for my $border (xlEdgeLeft, xlEdgeTop, xlEdgeBottom, xlEdgeRight, xlInsideVertical, xlInsideHorizontal) {
        with(
            $self->{Worksheet}->Range($map_range)->Borders($border),
            LineStyle  => xlContinuous,
            Weight     => xlThin,
            ColorIndex => xlAutomatic,
        );
    }
    $Excel->{ActiveWindow}{Zoom} = 0 + $self->{Workmap}{Zoom} if defined($self->{Workmap}{Zoom});
    return 1;
}

sub draw_cell {
    my $self = shift;
    my ($x, $y, $row, $col) = @_;
    my $value       = $self->{Workmap}{Matrix}{$x}{$y};
    my $map_option  = $self->{Workmap}{Map_Option};
    my $val_append  = $map_option & ADD_COORD() ? " ($x,$y)" : '';
    my $fill_value  = $value . $val_append;
    printf STDERR ("\r>Fill: IC [%3d,%3d] => Cell (%3d,%3d) => Value %-20s", $x, $y, $row, $col, "'$fill_value'");
    $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Value} = $fill_value;
    if (defined($self->{Workmap}{Ranges})) {
        my $color_index = $self->get_color_index($value);
        $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Interior}->{ColorIndex} = $color_index
            if $color_index >= 0;
    }
    return 1;
}

sub get_color_index {
    my $self = shift;
    my ($value) = @_;
    my $compare = $self->{Workmap}{Compare};
    my $colors = $self->{Workmap}{Colors};
    my $ranges = $self->{Workmap}{Ranges};
    my $na_color = (@$colors == @$ranges + 2) ? $colors->[$#$colors] : DEFAULT_NA_COLOR;
    my $other_color = ((@$colors > @$ranges)) ? $colors->[ scalar(@$ranges) ] : DEFAULT_OTHER_COLOR;
    if (defined($value) && $value ne '') {
        my $index = $self->{Workmap}{Comp_Sub}->($value, $ranges);
        $self->{Workmap}{Range_Count}[$index] += 1;
        if ($index >= scalar(@$ranges)) {
            return $other_color;
        } else {
            return $colors->[$index];
        }
    } else {
        return $na_color;
    }
}

sub fill_count_info {
    my $self = shift;
    my $counts = $self->{Workmap}{Range_Count};
    my $mode = $self->{Workmap}{Compare};
    my @ord_counts = @$counts;
    if ( !($mode & 4) && !($mode & 1) ) {
        my $last = pop @ord_counts;
        unshift @ord_counts, $last;
    }
    my $row = $self->{Workmap}{Limit_Row};
    my $col = 3;
    $self->{Worksheet}->Cells(0 + $row, 0 + $col)->{Value} = 'Count';
    $row++;
    for my $i (0 .. $#ord_counts) {
        $self->{Worksheet}->Cells(0 + $i + $row, 0 + $col)->{Value} = $ord_counts[$i];
    }
    return 1;
}

sub get_range_colors {
    my $self = shift;
    my $ranges = $self->{Workmap}{Ranges};
    my $colors = $self->{Workmap}{Colors};
    my $mode   = $self->{Workmap}{Compare};
    my $na_color = (@$colors == @$ranges + 2) ? $colors->[$#$colors] : DEFAULT_NA_COLOR;
    my $other_color = @$colors > @$ranges ? $colors->[ scalar(@$ranges) ] : DEFAULT_OTHER_COLOR;
    if ($mode & 4) {
        return [ @$colors ];
    }
    my @range_colors;
    push @range_colors, $other_color unless ($mode & 1);
    for my $i (0 .. $#$ranges) {
        push @range_colors, $colors->[$i];
    }
    push @range_colors, $other_color if ($mode & 1);
    return \@range_colors;
}

sub finish_map {
    my $self = shift;
    $self->{Workmap}{Matrix} = undef;
    $self->{Workmap} = undef;
    $self->{Worksheet} = undef;
    1;
}

### class method
use constant COL_LETTERS => [ '', 'A' .. 'IV' ];
sub R1C1_to_A1 {
    my ($row, $col) = @_;
    error("Invalid row/column specification: ($row, $col)") unless(0 <= $row && $row <= 65536 && 0 <= $col && $col <= 255);
    my $letter = +COL_LETTERS->[$col];
#    --$col;
#    my $indh = int($col / 26);
#    my $indl = $col % 26;
#    my $letter2 = ($indh ? chr(ord('A') + $indh - 1) : '') . chr(ord('A') + $indl);
    return $letter . $row;
}

1;
